/**
 * Created : Jun 7, 2012
 *
 * @author pquiring
 */

import java.awt.*;
import java.awt.datatransfer.*;
import java.io.*;
import java.util.*;
import javax.swing.*;
import javax.swing.tree.*;
import javax.swing.table.*;

import javaforce.*;

public class MainPanel extends javax.swing.JPanel {

  /**
   * Creates new form MainPanel
   */
  public MainPanel() {
    initComponents();
    This = this;
    safe.setUseUniqueNames(true);
    safe.setUseAttributeNameForName(true);
    safeGroups.setUseUniqueNames(true);
    safeGroups.setUseAttributeNameForName(true);
    safe.setRoot("group", "name=\"Passwords\"", "");
    loadConfig();
    copyGroups();
    groups.setModel(safeGroups.getTreeModel());
    table = (DefaultTableModel)entries.getModel();
    initDND();
  }

  /**
   * This method is called from within the constructor to initialize the form.
   * WARNING: Do NOT modify this code. The content of this method is always
   * regenerated by the Form Editor.
   */
  @SuppressWarnings("unchecked")
  // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
  private void initComponents() {

    jToolBar1 = new javax.swing.JToolBar();
    newSafe = new javax.swing.JButton();
    openSafe = new javax.swing.JButton();
    closeSafe = new javax.swing.JButton();
    saveSafe = new javax.swing.JButton();
    jSeparator1 = new javax.swing.JToolBar.Separator();
    settings = new javax.swing.JButton();
    jSeparator2 = new javax.swing.JToolBar.Separator();
    importFile = new javax.swing.JButton();
    exportFile = new javax.swing.JButton();
    jSeparator3 = new javax.swing.JToolBar.Separator();
    jButton2 = new javax.swing.JButton();
    jSeparator5 = new javax.swing.JToolBar.Separator();
    progress = new javax.swing.JProgressBar();
    jSeparator4 = new javax.swing.JToolBar.Separator();
    jSplitPane1 = new javax.swing.JSplitPane();
    jPanel1 = new javax.swing.JPanel();
    jToolBar2 = new javax.swing.JToolBar();
    jLabel1 = new javax.swing.JLabel();
    addGroup = new javax.swing.JButton();
    deleteGroup = new javax.swing.JButton();
    renameGroup = new javax.swing.JButton();
    jScrollPane3 = new javax.swing.JScrollPane();
    groups = new javax.swing.JTree();
    jPanel2 = new javax.swing.JPanel();
    jToolBar3 = new javax.swing.JToolBar();
    jLabel2 = new javax.swing.JLabel();
    addEntry = new javax.swing.JButton();
    deleteEntry = new javax.swing.JButton();
    editEntry = new javax.swing.JButton();
    copyUsername = new javax.swing.JButton();
    copyPassword = new javax.swing.JButton();
    jButton1 = new javax.swing.JButton();
    jScrollPane1 = new javax.swing.JScrollPane();
    entries = new javax.swing.JTable();

    jToolBar1.setFloatable(false);
    jToolBar1.setRollover(true);

    newSafe.setText("New");
    newSafe.setFocusable(false);
    newSafe.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
    newSafe.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
    newSafe.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        newSafeActionPerformed(evt);
      }
    });
    jToolBar1.add(newSafe);

    openSafe.setText("Open");
    openSafe.setFocusable(false);
    openSafe.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
    openSafe.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
    openSafe.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        openSafeActionPerformed(evt);
      }
    });
    jToolBar1.add(openSafe);

    closeSafe.setText("Close");
    closeSafe.setFocusable(false);
    closeSafe.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
    closeSafe.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
    closeSafe.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        closeSafeActionPerformed(evt);
      }
    });
    jToolBar1.add(closeSafe);

    saveSafe.setText("Save");
    saveSafe.setFocusable(false);
    saveSafe.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
    saveSafe.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
    saveSafe.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        saveSafeActionPerformed(evt);
      }
    });
    jToolBar1.add(saveSafe);
    jToolBar1.add(jSeparator1);

    settings.setText("Settings");
    settings.setFocusable(false);
    settings.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
    settings.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
    settings.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        settingsActionPerformed(evt);
      }
    });
    jToolBar1.add(settings);
    jToolBar1.add(jSeparator2);

    importFile.setText("Import");
    importFile.setFocusable(false);
    importFile.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
    importFile.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
    importFile.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        importFileActionPerformed(evt);
      }
    });
    jToolBar1.add(importFile);

    exportFile.setText("Export");
    exportFile.setFocusable(false);
    exportFile.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
    exportFile.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
    exportFile.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        exportFileActionPerformed(evt);
      }
    });
    jToolBar1.add(exportFile);
    jToolBar1.add(jSeparator3);

    jButton2.setText("Help");
    jButton2.setFocusable(false);
    jButton2.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
    jButton2.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
    jButton2.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        jButton2ActionPerformed(evt);
      }
    });
    jToolBar1.add(jButton2);
    jToolBar1.add(jSeparator5);
    jToolBar1.add(progress);
    jToolBar1.add(jSeparator4);

    jSplitPane1.setDividerLocation(250);
    jSplitPane1.setResizeWeight(0.3);

    jToolBar2.setFloatable(false);
    jToolBar2.setRollover(true);

    jLabel1.setText("Groups:");
    jToolBar2.add(jLabel1);

    addGroup.setText("Add");
    addGroup.setFocusable(false);
    addGroup.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
    addGroup.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
    addGroup.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        addGroupActionPerformed(evt);
      }
    });
    jToolBar2.add(addGroup);

    deleteGroup.setText("Delete");
    deleteGroup.setFocusable(false);
    deleteGroup.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
    deleteGroup.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
    deleteGroup.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        deleteGroupActionPerformed(evt);
      }
    });
    jToolBar2.add(deleteGroup);

    renameGroup.setText("Rename");
    renameGroup.setFocusable(false);
    renameGroup.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
    renameGroup.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
    renameGroup.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        renameGroupActionPerformed(evt);
      }
    });
    jToolBar2.add(renameGroup);

    javax.swing.tree.DefaultMutableTreeNode treeNode1 = new javax.swing.tree.DefaultMutableTreeNode("root");
    groups.setModel(new javax.swing.tree.DefaultTreeModel(treeNode1));
    groups.addTreeSelectionListener(new javax.swing.event.TreeSelectionListener() {
      public void valueChanged(javax.swing.event.TreeSelectionEvent evt) {
        groupsValueChanged(evt);
      }
    });
    jScrollPane3.setViewportView(groups);

    javax.swing.GroupLayout jPanel1Layout = new javax.swing.GroupLayout(jPanel1);
    jPanel1.setLayout(jPanel1Layout);
    jPanel1Layout.setHorizontalGroup(
      jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
      .addComponent(jToolBar2, javax.swing.GroupLayout.DEFAULT_SIZE, 251, Short.MAX_VALUE)
      .addComponent(jScrollPane3)
    );
    jPanel1Layout.setVerticalGroup(
      jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
      .addGroup(jPanel1Layout.createSequentialGroup()
        .addComponent(jToolBar2, javax.swing.GroupLayout.PREFERRED_SIZE, 25, javax.swing.GroupLayout.PREFERRED_SIZE)
        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
        .addComponent(jScrollPane3, javax.swing.GroupLayout.DEFAULT_SIZE, 481, Short.MAX_VALUE))
    );

    jSplitPane1.setLeftComponent(jPanel1);

    jToolBar3.setFloatable(false);
    jToolBar3.setRollover(true);

    jLabel2.setText("Sites:");
    jToolBar3.add(jLabel2);

    addEntry.setText("Add");
    addEntry.setFocusable(false);
    addEntry.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
    addEntry.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
    addEntry.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        addEntryActionPerformed(evt);
      }
    });
    jToolBar3.add(addEntry);

    deleteEntry.setText("Delete");
    deleteEntry.setFocusable(false);
    deleteEntry.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
    deleteEntry.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
    deleteEntry.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        deleteEntryActionPerformed(evt);
      }
    });
    jToolBar3.add(deleteEntry);

    editEntry.setText("Edit");
    editEntry.setFocusable(false);
    editEntry.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
    editEntry.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
    editEntry.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        editEntryActionPerformed(evt);
      }
    });
    jToolBar3.add(editEntry);

    copyUsername.setText("Copy Username");
    copyUsername.setToolTipText("Ctrl+Z");
    copyUsername.setFocusable(false);
    copyUsername.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
    copyUsername.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
    copyUsername.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        copyUsernameActionPerformed(evt);
      }
    });
    jToolBar3.add(copyUsername);

    copyPassword.setText("Copy Password");
    copyPassword.setToolTipText("Ctrl+C");
    copyPassword.setFocusable(false);
    copyPassword.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
    copyPassword.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
    copyPassword.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        copyPasswordActionPerformed(evt);
      }
    });
    jToolBar3.add(copyPassword);

    jButton1.setText("Open Site");
    jButton1.setFocusable(false);
    jButton1.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
    jButton1.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
    jButton1.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        jButton1ActionPerformed(evt);
      }
    });
    jToolBar3.add(jButton1);

    entries.setModel(new javax.swing.table.DefaultTableModel(
      new Object [][] {

      },
      new String [] {
        "Title", "User Name", "URL"
      }
    ) {
      Class[] types = new Class [] {
        java.lang.String.class, java.lang.String.class, java.lang.String.class
      };
      boolean[] canEdit = new boolean [] {
        false, false, false
      };

      public Class getColumnClass(int columnIndex) {
        return types [columnIndex];
      }

      public boolean isCellEditable(int rowIndex, int columnIndex) {
        return canEdit [columnIndex];
      }
    });
    entries.addMouseListener(new java.awt.event.MouseAdapter() {
      public void mouseClicked(java.awt.event.MouseEvent evt) {
        entriesMouseClicked(evt);
      }
    });
    entries.addKeyListener(new java.awt.event.KeyAdapter() {
      public void keyPressed(java.awt.event.KeyEvent evt) {
        entriesKeyPressed(evt);
      }
      public void keyTyped(java.awt.event.KeyEvent evt) {
        entriesKeyTyped(evt);
      }
    });
    jScrollPane1.setViewportView(entries);

    javax.swing.GroupLayout jPanel2Layout = new javax.swing.GroupLayout(jPanel2);
    jPanel2.setLayout(jPanel2Layout);
    jPanel2Layout.setHorizontalGroup(
      jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
      .addComponent(jToolBar3, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
      .addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 373, Short.MAX_VALUE)
    );
    jPanel2Layout.setVerticalGroup(
      jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
      .addGroup(jPanel2Layout.createSequentialGroup()
        .addComponent(jToolBar3, javax.swing.GroupLayout.PREFERRED_SIZE, 25, javax.swing.GroupLayout.PREFERRED_SIZE)
        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
        .addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 481, Short.MAX_VALUE))
    );

    jSplitPane1.setRightComponent(jPanel2);

    javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
    this.setLayout(layout);
    layout.setHorizontalGroup(
      layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
      .addComponent(jToolBar1, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
      .addComponent(jSplitPane1)
    );
    layout.setVerticalGroup(
      layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
      .addGroup(layout.createSequentialGroup()
        .addComponent(jToolBar1, javax.swing.GroupLayout.PREFERRED_SIZE, 25, javax.swing.GroupLayout.PREFERRED_SIZE)
        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
        .addComponent(jSplitPane1))
    );
  }// </editor-fold>//GEN-END:initComponents

  private void newSafeActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_newSafeActionPerformed
    newSafe();
  }//GEN-LAST:event_newSafeActionPerformed

  private void openSafeActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_openSafeActionPerformed
    openSafe();
  }//GEN-LAST:event_openSafeActionPerformed

  private void closeSafeActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_closeSafeActionPerformed
    closeSafe();
  }//GEN-LAST:event_closeSafeActionPerformed

  private void saveSafeActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_saveSafeActionPerformed
    saveSafe();
  }//GEN-LAST:event_saveSafeActionPerformed

  private void settingsActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_settingsActionPerformed
    editConfig();
  }//GEN-LAST:event_settingsActionPerformed

  private void addGroupActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_addGroupActionPerformed
    addGroup();
  }//GEN-LAST:event_addGroupActionPerformed

  private void deleteGroupActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_deleteGroupActionPerformed
    deleteGroup();
  }//GEN-LAST:event_deleteGroupActionPerformed

  private void renameGroupActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_renameGroupActionPerformed
    renameGroup();
  }//GEN-LAST:event_renameGroupActionPerformed

  private void addEntryActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_addEntryActionPerformed
    addEntry();
  }//GEN-LAST:event_addEntryActionPerformed

  private void deleteEntryActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_deleteEntryActionPerformed
    deleteEntry();
  }//GEN-LAST:event_deleteEntryActionPerformed

  private void editEntryActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_editEntryActionPerformed
    editEntry();
  }//GEN-LAST:event_editEntryActionPerformed

  private void importFileActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_importFileActionPerformed
    importFile();
  }//GEN-LAST:event_importFileActionPerformed

  private void exportFileActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_exportFileActionPerformed
    exportFile();
  }//GEN-LAST:event_exportFileActionPerformed

  private void groupsValueChanged(javax.swing.event.TreeSelectionEvent evt) {//GEN-FIRST:event_groupsValueChanged
    listEntries();
  }//GEN-LAST:event_groupsValueChanged

  private void entriesMouseClicked(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_entriesMouseClicked
    if (evt.getClickCount() == 2) editEntry();
  }//GEN-LAST:event_entriesMouseClicked

  private void entriesKeyPressed(java.awt.event.KeyEvent evt) {//GEN-FIRST:event_entriesKeyPressed
    int mod = evt.getModifiers();
    int kc = evt.getKeyChar();
    int cc = evt.getKeyCode();
//    JFLog.log("key:" + mod + "," + kc + "," + cc);
    if (kc == '\n' && mod == 0) {
      evt.consume();
      editEntry();
      return;
    }
    if (mod != java.awt.event.KeyEvent.CTRL_MASK) return;
    switch (cc) {
      case 'C': copyPassword(); evt.consume(); break;
      case 'Z': copyUsername(); evt.consume(); break;
      case 'O': openSite(); evt.consume(); break;
    }
  }//GEN-LAST:event_entriesKeyPressed

  private void copyPasswordActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_copyPasswordActionPerformed
    copyPassword();
  }//GEN-LAST:event_copyPasswordActionPerformed

  private void entriesKeyTyped(java.awt.event.KeyEvent evt) {//GEN-FIRST:event_entriesKeyTyped
    char ch = evt.getKeyChar();
    int len = table.getRowCount();
    int idx = entries.getSelectedRow();
    if (idx < 0) idx = 0;
    int start = idx;
    do {
      idx++;
      if (idx >= len) idx = 0;
      String title = (String)table.getValueAt(idx, 0);
      if (title.toLowerCase().charAt(0) == ch) {
        entries.changeSelection(idx, idx, false, false);
        return;
      }
    } while (idx != start);
  }//GEN-LAST:event_entriesKeyTyped

  private void copyUsernameActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_copyUsernameActionPerformed
    copyUsername();
  }//GEN-LAST:event_copyUsernameActionPerformed

  private void jButton1ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jButton1ActionPerformed
    openSite();
  }//GEN-LAST:event_jButton1ActionPerformed

  private void jButton2ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jButton2ActionPerformed
    showHelp();
  }//GEN-LAST:event_jButton2ActionPerformed

  // Variables declaration - do not modify//GEN-BEGIN:variables
  private javax.swing.JButton addEntry;
  private javax.swing.JButton addGroup;
  private javax.swing.JButton closeSafe;
  private javax.swing.JButton copyPassword;
  private javax.swing.JButton copyUsername;
  private javax.swing.JButton deleteEntry;
  private javax.swing.JButton deleteGroup;
  private javax.swing.JButton editEntry;
  private javax.swing.JTable entries;
  private javax.swing.JButton exportFile;
  private javax.swing.JTree groups;
  private javax.swing.JButton importFile;
  private javax.swing.JButton jButton1;
  private javax.swing.JButton jButton2;
  private javax.swing.JLabel jLabel1;
  private javax.swing.JLabel jLabel2;
  private javax.swing.JPanel jPanel1;
  private javax.swing.JPanel jPanel2;
  private javax.swing.JScrollPane jScrollPane1;
  private javax.swing.JScrollPane jScrollPane3;
  private javax.swing.JToolBar.Separator jSeparator1;
  private javax.swing.JToolBar.Separator jSeparator2;
  private javax.swing.JToolBar.Separator jSeparator3;
  private javax.swing.JToolBar.Separator jSeparator4;
  private javax.swing.JToolBar.Separator jSeparator5;
  private javax.swing.JSplitPane jSplitPane1;
  private javax.swing.JToolBar jToolBar1;
  private javax.swing.JToolBar jToolBar2;
  private javax.swing.JToolBar jToolBar3;
  private javax.swing.JButton newSafe;
  private javax.swing.JButton openSafe;
  private javax.swing.JProgressBar progress;
  private javax.swing.JButton renameGroup;
  private javax.swing.JButton saveSafe;
  private javax.swing.JButton settings;
  // End of variables declaration//GEN-END:variables

  public static MainPanel This;
  public static Config config;
  private String configFolder = JF.getUserPath();
  private String configFile = "/.jfpasswords.xml";
  private XML safe = new XML();
  private XML safeGroups = new XML();  //a copy with just groups
  protected static String password;
  private DefaultTableModel table;
  private java.util.Timer timer;
  private Object timerLock = new Object();
  private int timerCount;

  public boolean dirty = false;
  public long timestamp = -1;
  public boolean loaded = false;

  public static class Config {
    public String safe = "";
    public int passwordGeneratorLength = 12;
    public boolean passwordGeneratorSymbols = true;
    public boolean passwordGeneratorAmbiguous = true;
    public boolean reAuthOnShow = true;
    public boolean bWindowMax = false;
    public int WindowXSize = -1, WindowYSize = -1;
    public int WindowXPos = 0, WindowYPos = 0;
  }

  private void loadConfig() {
    config = new Config();
    try {
      XML xml = new XML();
      FileInputStream fis = new FileInputStream(configFolder + configFile);
      xml.read(fis);
      xml.writeClass(config);
    } catch (FileNotFoundException e1) {
      config = new Config();
    } catch (Exception e2) {
      JFLog.log(e2);
      config = new Config();
    }
  }

  private void saveConfig() {
    try {
      XML xml = new XML();
      FileOutputStream fos = new FileOutputStream(configFolder + configFile);
      xml.readClass("jpassword", config);
      xml.write(fos);
      fos.close();
    } catch (Exception e) {
      JFLog.log(e);
    }
  }

  private String getSafeName() {
    String name = config.safe;
    int idx = name.lastIndexOf("/");
    if (idx == -1) idx = 0; else idx++;
    return name.substring(idx, name.length() - 5);
  }

  public boolean loadSafe(boolean reopen) {
    if (!reopen) {
      password = GetPassword.getPassword(null);
      if (password == null) return false;
    } else {
      if (password == null) return false;
    }
    try {
      safe.deleteAll();
      File file = new File(config.safe);
      timestamp = file.lastModified();
      FileInputStream fis = new FileInputStream(config.safe);
      byte encrypted[] = JF.readAll(fis);
      fis.close();
      Data.setPassword(password);
      byte decrypted[] = Data.decrypt(encrypted);
      if (decrypted == null) {
        throw new Exception("decrypt failed");
      }
      safe.read(new ByteArrayInputStream(decrypted));
      safe.setRoot("group", "name=\"" + getSafeName() + "\"", "");
      copyGroups();
      dirty = false;
      loaded = true;
      return true;
    } catch (Exception e) {
      JFAWT.showError("Error", "Wrong Password");
      safe.deleteAll();
      password = null;
      timestamp = -1;
      dirty = false;
      JFLog.log(e);
      return false;
    }
  }

  private void copyGroups() {
    safeGroups.deleteAll();
    safeGroups.setRoot("group", "name=\"" + safe.root.getArg("name") + "\"", "");
    copyGroups(safe.root, safeGroups.root);
  }

  private void copyGroups(XML.XMLTag src, XML.XMLTag dst) {
    int cnt = src.getChildCount();
    groups.makeVisible(new TreePath(dst.getPath()));
    for(int a=0;a<cnt;a++) {
      XML.XMLTag tag = src.getChildAt(a);
      if (tag.name.equals("group")) {
        XML.XMLTag newtag = safeGroups.addTag(dst, "group", "name=\"" + tag.getArg("name") + "\"", "");
        copyGroups(tag, newtag);
        continue;
      }
    }
  }

  public boolean saveSafe() {
    if (config.safe.length() == 0) {
      JFileChooser chooser = new JFileChooser();
      chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
      chooser.setMultiSelectionEnabled(false);
      chooser.setCurrentDirectory(new File(JF.getUserPath() + "/Documents"));
      javax.swing.filechooser.FileFilter ffSafe = new javax.swing.filechooser.FileFilter() {
        public boolean accept(File file) {
          if (file.isDirectory()) return true;
          if (file.getName().endsWith(".safe")) return true;
          return false;
        }
        public String getDescription() {
          return "Safe (*.safe)";
        }
      };
      chooser.addChoosableFileFilter(ffSafe);
      chooser.setFileFilter(ffSafe);
      if (chooser.showSaveDialog(this) != JFileChooser.APPROVE_OPTION) return false;
      config.safe = chooser.getSelectedFile().getAbsolutePath();
      if (!config.safe.endsWith(".safe")) config.safe += ".safe";
      safe.setRoot("group", "name=\"" + getSafeName() + "\"", "");
      copyGroups();
    }
    if (password == null) {
      String pass1 = GetPassword.getPassword(null, "Create Password");
      String pass2 = GetPassword.getPassword(null, "Confirm Password");
      if (!pass1.equals(pass2)) {
        JFAWT.showError("Error", "Passwords do not match");
        return false;
      }
      password = pass1;
    }
    try {
      FileOutputStream fos = new FileOutputStream(config.safe);
      ByteArrayOutputStream baos = new ByteArrayOutputStream();
      safe.write(baos);
      Data.setPassword(password);
      byte encrypted[] = Data.encrypt(baos.toByteArray());
      fos.write(encrypted);
      fos.close();
      File file = new File(config.safe);
      timestamp = file.lastModified();
      dirty = false;
      saveConfig();
      loaded = true;
      return true;
    } catch (Exception e) {
      JFAWT.showError("Error", "Failed to save safe!\nError:" + e.toString());
      JFLog.log(e);
      return false;
    }
  }

  private void openSafe() {
    if (dirty) {
      switch (JFAWT.showConfirm3("Confirm", "Save first?")) {
        case JFAWT.YES: saveSafe(); break;
        case JFAWT.NO: break;
        case JFAWT.CANCEL: return;
      }
    }
    JFileChooser chooser = new JFileChooser();
    chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
    chooser.setMultiSelectionEnabled(false);
    chooser.setCurrentDirectory(new File(JF.getUserPath() + "/Documents"));
    javax.swing.filechooser.FileFilter ffSafe = new javax.swing.filechooser.FileFilter() {
      public boolean accept(File file) {
        if (file.isDirectory()) return true;
        if (file.getName().endsWith(".safe")) return true;
        return false;
      }
      public String getDescription() {
        return "Safe (*.safe)";
      }
    };
    chooser.addChoosableFileFilter(ffSafe);
    chooser.setFileFilter(ffSafe);
    if (chooser.showOpenDialog(this) != JFileChooser.APPROVE_OPTION) return;
    config.safe = chooser.getSelectedFile().getAbsolutePath();
    saveConfig();
    loadSafe(false);
  }

  private void newSafe() {
    if (dirty) {
      switch (JFAWT.showConfirm3("Confirm", "Save first?")) {
        case JFAWT.YES: saveSafe(); break;
        case JFAWT.NO: break;
        case JFAWT.CANCEL: return;
      }
    }
    safe.deleteAll();
    safe.setRoot("group", "name=\"Passwords\"", "");
    config.safe = "";
    password = null;
    timestamp = -1;
    saveConfig();
    copyGroups();
    loaded = false;
  }

  private void closeSafe() {
    newSafe();
  }

  private void editConfig() {
    EditConfig dialog = new EditConfig(null, true);
    dialog.setVisible(true);
    if (dialog.accepted) saveConfig();
  }

  private void addGroup() {
    String name = JFAWT.getString("Enter Group Name:", "");
    if (name == null) return;
    TreePath path = groups.getSelectionPath();
    XML.XMLTag selTag;
    if (path == null) {
      selTag = safe.root;
    } else {
      selTag = safe.getTag(path);
    }
    safe.addTag(selTag, "group", "name=\"" + name + "\"", "");
    copyGroups();
    dirty = true;
  }

  private void deleteGroup() {
    TreePath path = groups.getSelectionPath();
    XML.XMLTag selTag;
    if (path == null) return;
    selTag = safe.getTag(path);
    if (!JFAWT.showConfirm("Confirm", "Are you sure?")) return;
    safe.deleteTag(selTag);
    copyGroups();
    dirty = true;
  }

  private void renameGroup() {
    TreePath path = groups.getSelectionPath();
    XML.XMLTag selTag;
    if (path == null) return;
    selTag = safe.getTag(path);
    String newName = JFAWT.getString("Enter new name", selTag.getArg("name"));
    if ((newName == null) || (newName.length() == 0)) return;
    selTag.setArg("name", newName);
    copyGroups();
    dirty = true;
  }

  private static class Entry implements Comparable {
    public String title, username, url;
    public int compareTo(Object o) {
      Entry e = (Entry)o;
      return title.compareTo(e.title);
    }
  }

  private void listEntries() {
    table.setRowCount(0);
    TreePath path = groups.getSelectionPath();
    XML.XMLTag selTag;
    if (path == null) {
      selTag = safe.root;
    } else {
      selTag = safe.getTag(path);
    }
    if (selTag == null) return;
    int cnt = selTag.getChildCount();
    ArrayList<Entry> alist = new ArrayList<Entry>();
    for(int a=0;a<cnt;a++) {
      XML.XMLTag child = selTag.getChildAt(a);
      if (!child.name.equals("entry")) continue;
      int cnt2 = child.getChildCount();
      String title = child.getArg("name"), username = "", pass = "", url = "", notes = "";
      for(int b=0;b<cnt2;b++) {
        XML.XMLTag f = child.getChildAt(b);
        if (f.name.equals("username")) {
          username = f.getContent();
          continue;
        }
        if (f.name.equals("password")) {
          pass = f.getContent();
          continue;
        }
        if (f.name.equals("url")) {
          url = f.getContent();
          continue;
        }
        if (f.name.equals("notes")) {
          notes = f.getContent();
          continue;
        }
      }
      Entry e = new Entry();
      e.title = title;
      e.username = username;
      e.url = url;
      alist.add(e);
    }
    Entry[] elist = alist.toArray(new Entry[0]);
    Arrays.sort(elist);
    for(int a=0;a<elist.length;a++) {
      Entry e = elist[a];
      table.addRow(new Object[] {e.title, e.username, e.url});
    }
  }

  private void addEntry() {
    EditEntry dialog = new EditEntry(null, true, null, table, -1);
    dialog.setVisible(true);
    if (!dialog.accepted) return;
    TreePath path = groups.getSelectionPath();
    XML.XMLTag selTag;
    if (path == null) {
      selTag = safe.root;
    } else {
      selTag = safe.getTag(path);
    }
    XML.XMLTag entry = safe.addTag(selTag, "entry", "", "");
    dialog.saveTo(safe, entry);
    listEntries();
    dirty = true;
  }

  private void deleteEntry() {
    TreePath path = groups.getSelectionPath();
    if (path == null) return;
    XML.XMLTag selTag;
    selTag = safe.getTag(path);
    int row = entries.getSelectedRow();
    if (row == -1) return;
    String title = (String)entries.getValueAt(row, 0);
    int cnt = selTag.getChildCount();
    XML.XMLTag child = null;
    for(int a=0;a<cnt;a++) {
      child = selTag.getChildAt(a);
      if (child.name.equals("entry") && child.getArg("name").equals(title)) break;
      child = null;
    }
    if (child == null) return;
    if (!JFAWT.showConfirm("Confirm", "Are you sure?")) return;
    safe.deleteTag(child);
    listEntries();
    dirty = true;
  }

  private void editEntry() {
    TreePath path = groups.getSelectionPath();
    XML.XMLTag selTag;
    if (path == null) return;
    selTag = safe.getTag(path);
    int row = entries.getSelectedRow();
    if (row == -1) return;
    String title = (String)entries.getValueAt(row, 0);
    int cnt = selTag.getChildCount();
    boolean ok = false;
    XML.XMLTag child = null;
    for(int a=0;a<cnt;a++) {
      child = selTag.getChildAt(a);
      if (child.getArg("name").equals(title)) {
        ok = true;
        break;
      }
    }
    if (!ok) return;
    EditEntry dialog = new EditEntry(null, true, child, table, row);
    dialog.setVisible(true);
    if (!dialog.accepted) return;
    dialog.saveTo(safe, child);
    listEntries();
    dirty = true;
  }

  private void importFile() {
    try {
      JFileChooser chooser = new JFileChooser();
      chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
      chooser.setMultiSelectionEnabled(false);
      chooser.setCurrentDirectory(new File(JF.getUserPath() + "/Documents"));
      javax.swing.filechooser.FileFilter ffKeePass = new javax.swing.filechooser.FileFilter() {
        public boolean accept(File file) {
          if (file.isDirectory()) return true;
          if (file.getName().endsWith(".xml")) return true;
          return false;
        }
        public String getDescription() {
          return "KeePass 2.x (*.xml)";
        }
      };
      chooser.addChoosableFileFilter(ffKeePass);
      javax.swing.filechooser.FileFilter ffUnSafe = new javax.swing.filechooser.FileFilter() {
        public boolean accept(File file) {
          if (file.isDirectory()) return true;
          if (file.getName().endsWith(".unsafe")) return true;
          return false;
        }
        public String getDescription() {
          return "jPasswords (*.unsafe)";
        }
      };
      chooser.addChoosableFileFilter(ffKeePass);
      chooser.setFileFilter(ffKeePass);
      if (chooser.showOpenDialog(this) != JFileChooser.APPROVE_OPTION) return;
      File file = chooser.getSelectedFile();
      if (file.getName().endsWith(".xml")) {
        importFileKeePass2(file);
        return;
      }
      if (file.getName().endsWith(".unsafe")) {
        importFileUnSafe(file);
        return;
      }
    } catch (Exception e) {
      JFLog.log(e);
      JFAWT.showError("Error", "Import failed");
    }
  }

  private void importFileKeePass2(File file) {
    try {
      FileInputStream fis = new FileInputStream(file);
      XML xml = new XML();
      xml.read(fis);
      fis.close();
      //find the root tag
      XML.XMLTag root = null;
      int cnt=xml.root.getChildCount();
      for(int a=0;a<cnt;a++) {
        XML.XMLTag child = xml.root.getChildAt(a);
        if (child.name.equals("Root")) {
          root = child;
          break;
        }
      }
      if (root == null) throw new Exception("Root tag not found");
      XML.XMLTag dst = safe.root;
      importGroup(root, dst);
      copyGroups();
    } catch (Exception e) {
      JFLog.log(e);
      JFAWT.showError("Error", "Import failed");
    }
  }

  private void importGroup(XML.XMLTag src, XML.XMLTag dst) {
    int cnt = src.getChildCount();
    for(int a=0;a<cnt;a++) {
      XML.XMLTag srcChild = src.getChildAt(a);
      if (srcChild.name.equals("Group")) {
        XML.XMLTag dstChild = safe.addTag(dst, "group", "", "");
        importGroup(srcChild, dstChild);
        continue;
      }
      if (srcChild.name.equals("Name")) {
        dst.setArg("name", srcChild.getContent());
        continue;
      }
      if (srcChild.name.equals("Entry")) {
        XML.XMLTag dstChild = safe.addTag(dst, "entry", "", "");
        importEntry(srcChild, dstChild);
        continue;
      }
    }
  }

  private void importEntry(XML.XMLTag src, XML.XMLTag dst) {
    int cnt = src.getChildCount();
    for(int a=0;a<cnt;a++) {
      XML.XMLTag child = src.getChildAt(a);
      if (child.name.equals("String")) {
        String Key = "", Value = "";
        int cntChild = child.getChildCount();
        for(int b=0;b<cntChild;b++) {
          XML.XMLTag child2 = child.getChildAt(b);
          if (child2.name.equals("Key")) {
            Key = child2.getContent();
            continue;
          }
          if (child2.name.equals("Value")) {
            Value = child2.getContent();
            continue;
          }
        }
        if (Key.equals("Title")) {
          dst.setArg("name", Value);
          continue;
        }
        if (Key.equals("UserName")) {
          safe.addTag(dst, "username", "", Value);
          continue;
        }
        if (Key.equals("Password")) {
          safe.addTag(dst, "password", "", Value);
          continue;
        }
        if (Key.equals("URL")) {
          safe.addTag(dst, "url", "", Value);
          continue;
        }
        if (Key.equals("Notes")) {
          safe.addTag(dst, "notes", "", Value);
          continue;
        }
      }
    }
  }

  private void importFileUnSafe(File file) {
    try {
      FileInputStream fis = new FileInputStream(file);
      safe.read(fis);
      fis.close();
      copyGroups();
    } catch (Exception e) {
      JFLog.log(e);
      JFAWT.showError("Error", "Import failed");
    }
  }

  private void exportFile() {
    try {
      JFileChooser chooser = new JFileChooser();
      chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
      chooser.setMultiSelectionEnabled(false);
      chooser.setCurrentDirectory(new File(JF.getUserPath() + "/Documents"));
      javax.swing.filechooser.FileFilter ffunSafe = new javax.swing.filechooser.FileFilter() {
        public boolean accept(File file) {
          if (file.isDirectory()) return true;
          if (file.getName().endsWith(".unsafe")) return true;
          return false;
        }
        public String getDescription() {
          return "jPasswords (*.unsafe)";
        }
      };
      chooser.addChoosableFileFilter(ffunSafe);
      chooser.setFileFilter(ffunSafe);
      if (chooser.showOpenDialog(this) != JFileChooser.APPROVE_OPTION) return;
      String filename = chooser.getSelectedFile().getAbsolutePath();
      if (!filename.endsWith(".unsafe")) filename += ".unsafe";
      FileOutputStream fos = new FileOutputStream(filename);
      safe.write(fos);
      fos.close();
    } catch (Exception e) {
      JFLog.log(e);
      JFAWT.showError("Error", "Export failed");
    }
  }

  private void copyPassword() {
    String pass = null;
    TreePath path = groups.getSelectionPath();
    XML.XMLTag selTag;
    if (path == null) return;
    selTag = safe.getTag(path);
    int row = entries.getSelectedRow();
    if (row == -1) return;
    String title = (String)entries.getValueAt(row, 0);
    int cnt = selTag.getChildCount();
    boolean ok = false;
    XML.XMLTag child = null;
    for(int a=0;a<cnt;a++) {
      child = selTag.getChildAt(a);
      if (child.getArg("name").equals(title)) {
        ok = true;
        break;
      }
    }
    if (!ok) return;
    int cntChild = child.getChildCount();
    XML.XMLTag f;
    for(int a=0;a<cntChild;a++) {
      f = child.getChildAt(a);
      if (f.name.equals("password")) {
        pass = f.getContent();
        break;
      }
    }
    if (pass == null) return;
    JFLog.log("Copy password to clipboard");
    StringSelection ss = new StringSelection(pass);
    Clipboard cb = Toolkit.getDefaultToolkit().getSystemClipboard();
    if (cb == null) return;
    cb.setContents(ss, ss);
    startTimer();
  }

  private void copyUsername() {
    String username = null;
    TreePath path = groups.getSelectionPath();
    XML.XMLTag selTag;
    if (path == null) return;
    selTag = safe.getTag(path);
    int row = entries.getSelectedRow();
    if (row == -1) return;
    String title = (String)entries.getValueAt(row, 0);
    int cnt = selTag.getChildCount();
    boolean ok = false;
    XML.XMLTag child = null;
    for(int a=0;a<cnt;a++) {
      child = selTag.getChildAt(a);
      if (child.getArg("name").equals(title)) {
        ok = true;
        break;
      }
    }
    if (!ok) return;
    int cntChild = child.getChildCount();
    XML.XMLTag f;
    for(int a=0;a<cntChild;a++) {
      f = child.getChildAt(a);
      if (f.name.equals("username")) {
        username = f.getContent();
        break;
      }
    }
    if (username == null) return;
    JFLog.log("Copy username to clipboard");
    StringSelection ss = new StringSelection(username);
    Clipboard cb = Toolkit.getDefaultToolkit().getSystemClipboard();
    if (cb == null) return;
    cb.setContents(ss, ss);
    startTimer();
  }

  public void openSite() {
    String url = null;
    TreePath path = groups.getSelectionPath();
    XML.XMLTag selTag;
    if (path == null) return;
    selTag = safe.getTag(path);
    int row = entries.getSelectedRow();
    if (row == -1) return;
    String title = (String)entries.getValueAt(row, 0);
    int cnt = selTag.getChildCount();
    boolean ok = false;
    XML.XMLTag child = null;
    for(int a=0;a<cnt;a++) {
      child = selTag.getChildAt(a);
      if (child.getArg("name").equals(title)) {
        ok = true;
        break;
      }
    }
    if (!ok) return;
    int cntChild = child.getChildCount();
    XML.XMLTag f;
    for(int a=0;a<cntChild;a++) {
      f = child.getChildAt(a);
      if (f.name.equals("url")) {
        url = f.getContent();
        break;
      }
    }
    if (url == null) return;
    JFLog.log("Open Site:" + url);
    if (!url.startsWith("http:") && !url.startsWith("https:")) {
      url = "http://" + url;
    }
    JFAWT.openURL(url);
  }

  public void showHelp() {
    JFAWT.showMessage("Help", "Ctrl+C = Copy Password\nCtrl+Z = Copy Username\nCtrl+O = Open Site");
  }

  public void startTimer() {
    synchronized(timerLock) {
      timerCount = 100;
      if (timer != null) return;
      timer = new java.util.Timer();
      timer.schedule(new java.util.TimerTask() {
        public void run() {
          java.awt.EventQueue.invokeLater(new Runnable() {
            public void run () {
              synchronized(timerLock) {
                timerCount -= 1;
                progress.setValue(timerCount);
                if (timerCount == 0) {
                  timer.cancel();
                  timer = null;
                  JFLog.log("clearing password");
                  StringSelection ss = new StringSelection("");
                  Clipboard cb = Toolkit.getDefaultToolkit().getSystemClipboard();
                  if (cb == null) return;
                  cb.setContents(ss, ss);
                }
              }
            }
          });
        }
      }, 100, 100);
    }
  }

  /** Checks if password safe file has changed, and reload if so. */
  public void checkTimestamp() {
    if (timestamp == -1) return;
    File file = new File(config.safe);
    if (!file.exists()) return;
    long newtimestamp = file.lastModified();
    if (newtimestamp != timestamp) {
      if (dirty) {
        if (!JFAWT.showConfirm("Reload", "Database file has changed with unsaved changes!\nReload?")) {
          return;
        }
      } else {
        if (!JFAWT.showConfirm("Reload", "Database file has changed!\nReload?")) {
          return;
        }
      }
      loadSafe(true);
    }
  }

  private class EntryFlavor extends DataFlavor {
    public EntryFlavor() throws ClassNotFoundException {
      super("text/custom1");
    }
    public XML.XMLTag group;
    public XML.XMLTag entries[];
  }

  private EntryFlavor ef;

  private class EntryTransfer implements Transferable {
    public EntryFlavor data;
    public EntryTransfer() {
      try {
        data = new EntryFlavor();
      } catch (Exception e) {
        JFLog.log(e);
      }
    }
    public DataFlavor[] getTransferDataFlavors() {
      return new DataFlavor[] {ef};
    }
    public boolean isDataFlavorSupported(DataFlavor flavor) {
      return (flavor instanceof EntryFlavor);
    }
    public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException {
//      if (!(flavor instanceof EntryFlavor)) return null;
      return data;
    }
  }


  private class GroupFlavor extends DataFlavor {
    public GroupFlavor() throws ClassNotFoundException {
      super("text/custom2");
    }
    public XML.XMLTag group;
  }

  private GroupFlavor gf;

  private class GroupTransfer implements Transferable {
    public GroupFlavor data;
    public GroupTransfer() {
      try {
        data = new GroupFlavor();
      } catch (Exception e) {
        JFLog.log(e);
      }
    }
    public DataFlavor[] getTransferDataFlavors() {
      return new DataFlavor[] {gf};
    }
    public boolean isDataFlavorSupported(DataFlavor flavor) {
      return (flavor instanceof GroupFlavor);
    }
    public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException {
//      if (!(flavor instanceof EntryFlavor)) return null;
      return data;
    }
  }

  private void initDND() {
    try {
      ef = new EntryFlavor();
      gf = new GroupFlavor();
    } catch (Exception e) {
      JFLog.log(e);
    }
    //right side (entries)
    entries.setDragEnabled(true);
    entries.setTransferHandler(
      new TransferHandler() {
        public boolean canImport(TransferHandler.TransferSupport info) {
//          JFLog.log("entries:canImport:" + info);
          return false;
        }
        public boolean importData(TransferHandler.TransferSupport info) {
//          JFLog.log("entries:importData:" + info);
          return false;
        }
        public int getSourceActions(JComponent c) {
//          JFLog.log("entries:getSourceActions");
          return MOVE;
        }
        protected Transferable createTransferable(JComponent c) {
//          JFLog.log("entries:createTransferable");
          EntryTransfer t = new EntryTransfer();

          TreePath path = groups.getSelectionPath();
          XML.XMLTag group;
          if (path == null) return null;
          group = safe.getTag(path);
          int rows[] = entries.getSelectedRows();
          t.data.entries = new XML.XMLTag[rows.length];
          for(int row=0;row<rows.length;row++) {
            String title = (String)entries.getValueAt(rows[row], 0);
            int cnt = group.getChildCount();
            boolean ok = false;
            XML.XMLTag child = null;
            for(int a=0;a<cnt;a++) {
              child = group.getChildAt(a);
              if (child.getArg("name").equals(title)) {
                ok = true;
                break;
              }
            }
            if (!ok) return null;
            t.data.entries[row] = child;
          }
          t.data.group = group;

          return t;
        }
        protected void exportDone(JComponent source, Transferable data, int action) {
//          JFLog.log("entries:exportDone:" + data);
          if (action != MOVE) return;
        }
      }
    );
    //left side (groups)
    groups.setDragEnabled(true);
//    groups.setDropMode(DropMode.USE_SELECTION);
    groups.setTransferHandler(
      new TransferHandler() {
        public boolean canImport(TransferHandler.TransferSupport info) {
//          JFLog.log("groups:canImport:" + info);
          if (!info.isDataFlavorSupported(ef) && !info.isDataFlavorSupported(gf)) {
//            JFLog.log("groups:canImport:false");
            return false;
          }
          try {
            Transferable t = info.getTransferable();
//            JFLog.log("t=" + t.getTransferData(ef));
          } catch (Exception e) {
            JFLog.log(e);
          }
          return true;
        }
        public boolean importData(TransferHandler.TransferSupport info) {
//          JFLog.log("groups:importData=" + info);
          if (info.isDataFlavorSupported(ef)) {
            try {
              EntryFlavor data = (EntryFlavor)info.getTransferable().getTransferData(ef);

              TreePath path = groups.getSelectionPath();
              XML.XMLTag dest;
              if (path == null) return false;
              dest = safe.getTag(path);
              if (dest == null) return false;

              if (dest == data.group) {
                JFLog.log("src == dest");
                return false;
              }

              //move data.entries to dest tag
              JFLog.log("moving " + data.entries.length + " entries");
              JFLog.log(" src=" + data.group);
              JFLog.log(" dest=" + dest);
              for(int a=0;a<data.entries.length;a++) {
                //do not use setParent() it does not update child lists
                //instead use add() which updates child lists in src and dest
                dest.add(data.entries[a]);
              }

              listEntries();  //refresh

              return true;
            } catch (Exception e) {
              JFLog.log(e);
            }
          }
          if (info.isDataFlavorSupported(gf)) {
            try {
              GroupFlavor data = (GroupFlavor)info.getTransferable().getTransferData(gf);

              TreePath path = groups.getSelectionPath();
              XML.XMLTag dest;
              if (path == null) return false;
              dest = safe.getTag(path);
              if (dest == null) return false;

              if (dest == data.group) {
                JFLog.log("src == dest");
                return false;
              }

              dest.add(data.group);

              copyGroups();

              return true;
            } catch (Exception e) {
              JFLog.log(e);
            }
          }
          return false;
        }
        public int getSourceActions(JComponent c) {
          return MOVE;
        }
        protected Transferable createTransferable(JComponent c) {
          GroupTransfer t = new GroupTransfer();

          TreePath path = groups.getSelectionPath();
          if (path == null) return null;
          t.data.group = safe.getTag(path);

          return t;
        }
        protected void exportDone(JComponent source, Transferable data, int action) {
        }
      }
    );
  }
}
